@import("js.sql");

const util = require("util");

const labInspectInterval = 1500;
const EventEmitter       = require("events").EventEmitter;

var Connector = function(documentPath, options) {

    this.documentPath = documentPath;
    this.options = @.merge.advanced({
        "!defaultValue": {},
        "!valueType": "object",
        "name": {
            "!valueType": "string",
            "!defaultValue": "lab",
        },
        "engine" : {
            "!valueType": "string",
            "!defaultValue": "sqlite",
        },
        "default": {
            "!valueType": "boolean",
            "!defaultValue": false,
        }
    }, options);

    this.id = 0;
    this.nextID = 1;
    this.models  = {};
    this.files = {};
    this.async = @.async.resolve();
    this.redis = {};

    this.ws_providers = {};
    this.ws_consumers = {};
    this.ws_scheduler = {};

    this.services = {};
    this.bindings = {};
    this.wrappers = {};
    this.constants = {};

    this.errors      = {};
    this.error_codes = {};
    this.last_update = 0;

    this.qos_monitor  = {
        last_minute     : 0,
        last_rpm        : 0,
        last_apm        : 0,
        last_epm        : 0,
        last_spm        : 0,

        current_rpm      : 0,
        current_epm      : 0,
        current_dpm      : 0,
        current_sessions : new Map(),
    };

    switch (this.options.engine) {
        case "mysql": {

            if (options.master) {

                this.sqlite = @mysql({
                    "host": options.master.host, 
                    "port": options.master.port, 
                    "username": options.master.username, 
                    "password": options.master.password, 
                    "database": options.master.database
                });

            } else {

                this.sqlite = @mysql({
                    "host": options.host, 
                    "port": options.port, 
                    "username": options.username, 
                    "password": options.password, 
                    "database": options.database
                });
                
            }
            break;
        }

        case "sqlite":  {
            this.sqlite = @sqlite(@path(documentPath, this.options.name + ".sqlite"));
            break;
        }
        default: {
            throw new Error(`Unknown database engine[${options.engine}]`);
        }
    }

}

@.inherits(Connector, EventEmitter);

var findObjectID = function(object) {
    var objectID;

    if (@.is(object, Object)) {
        if (object.id) {
            objectID = object.id;
        } else {
            throw new Error("object id not found");
        }
    } else if (object && @.is(object, String)) {
        objectID = object;
    } else {
        throw new Error("object id not found");
    }
    return objectID;
}

Connector.prototype.getRequestQOS = function () {

    if (Date.now() - this.qos_monitor.last_minute > 120000)   {

        return {
            rpm : 0,
            dpm : 0,
            epm : 0,
            spm : 0
        };

    } else {

        return {
            rpm : this.qos_monitor.last_rpm,
            dpm : this.qos_monitor.last_dpm,
            epm : this.qos_monitor.last_epm,
            spm : this.qos_monitor.last_spm
        };

    }
};

Connector.prototype.onRequestFinished = function({error, session, result, method, params, start_time, end_time, time_used}) {

    process.nextTick(() => {

        this.qos_monitor.current_rpm = this.qos_monitor.current_rpm + 1;
        this.qos_monitor.current_dpm = this.qos_monitor.current_dpm + time_used;

        if (error.code) {
            this.qos_monitor.current_rpm = this.qos_monitor.current_rpm + 1;
        }

        if (session.id) {
            this.qos_monitor.current_sessions.set(session.id,true);
        }

        if (Date.now() - this.qos_monitor.last_minute > 60000) {

            this.qos_monitor.last_rpm  = this.qos_monitor.current_rpm;
            this.qos_monitor.last_dpm  = this.qos_monitor.current_dpm / this.qos_monitor.current_rpm;
            this.qos_monitor.last_epm  = this.qos_monitor.current_epm;

            this.qos_monitor.current_rpm = 0;
            this.qos_monitor.current_dpm = 0;
            this.qos_monitor.current_epm = 0;

            this.qos_monitor.last_spm         = this.qos_monitor.current_sessions.size;
            this.qos_monitor.current_sessions.clear();

            this.qos_monitor.last_minute = Date.now();
            
        }

        @mew.auto("lab.request_finished", {error, session, result, method, params, start_time, end_time, time_used});
    });
};

Connector.prototype.inspect = function() {
    var lab = this;

    return @.async(function() {

        var apiExportedInfo = {
            
            services: @.keys(lab.services).reduce(function(result, domain) {

                @.keys(lab.services[domain]).filter(function(subdomain) {
                    return subdomain && subdomain[0] != "@";
                }).forEach(function(subdomain) {

                    if (@.is(lab.services[domain][subdomain], String)) {

                        if (lab.bindings[lab.services[domain][subdomain]]) {
                            result.push(domain + "." + subdomain + "[" + (lab.wrappers[domain][subdomain].length - 1) + "]");
                        }

                    } else {

                        @.keys(lab.services[domain][subdomain]).filter(function(subsubdomain) {
                            return subsubdomain && subsubdomain[0] != "@";
                        }).forEach(function(subsubdomain) {
                            var binding = lab.bindings[lab.services[domain][subdomain][subsubdomain]];
                            if (binding && !util.types.isProxy(binding)) {
                                result.push(domain + "@" + subdomain + "." + subsubdomain + "[" + (lab.wrappers[domain + "@" + subdomain][subsubdomain].length - 1) + "]");
                            }
                        });

                    }
                });

                return result;
            }, []),
            errors: @.keys(lab.errors).reduce(function(result, domain) {
                result[domain] = lab.errors[domain].message;
                return result;
            }, {}),
            constants: lab.constants,
            error_codes: @.keys(lab.error_codes).reduce(function(result, code) {

                result[code] = lab.error_codes[code];
                result[code] = result[code].message || result[code];

                return result;
            }, {})
        };


        var dataExportedInfo = {
            models: lab.models,
            wrappers  : @.keys(lab.wrappers).reduce(function (result,domain) {
                
                @.keys(lab.wrappers[domain]).forEach(function (api) {
                    result.push(domain.split("@").join(".") + "." + api);
                });

                return result;
            },[])
        };

        return @.lock(lab, labInspectInterval,function () {
            return @.async(function () {
                setTimeout(() => {
                    @.fs.writeFile(@.fs.realOSPath(lab.documentPath, lab.options.name + ".json"), JSON.stringify(@.merge.simple(apiExportedInfo, dataExportedInfo), null, 4)).resolve(apiExportedInfo).pipe(this);            
                }, labInspectInterval * 2);
            })
        })
    })
}

Connector.prototype.cached = function(model, id) {
    if (model && id && this.cache_pool) {
        return this.cache_pool.get(`${model}-${id}`);
    } else {
        return null;
    }
}

Connector.prototype.cache = function(model, object) {
    if (model && object && this.cache_pool) {
        this.cache_pool.put(`${model}-${object.id}`, object);
        return object;
    } else {
        return object;
    }
}

Connector.prototype.uncache = function(model, object) {
    if (model && object && this.cache_pool) {
        return this.cache_pool.remove(`${model}-${findObjectID(object)}`);
    } else {
        return false;
    }
};

Connector.prototype.cacheObject = function(model, object) {
    if (this.id && this.objects) {
        if (!this.objects[model]) {
            this.objects[model] = {};
        }
        this.objects[model][object.id] = object;
    }
    return object;
};


Connector.prototype.uncacheObject = function(model, object) {
    if (this.id && this.objects) {
        if (this.objects[model]) {
            delete this.objects[model][object.id];
        }
    }
    return object;
};


Connector.prototype.delete = function(model, object) {
    var lab = this;

    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model not found");
    }

    var objectID = findObjectID(object);

    return lab.find(model, objectID).then(function(cached) {

        if (cached) {

            lab.uncacheObject(model, cached);
            lab.sqlite.remove(model, cached).pipe(this);

        } else {
            this.reject("object not found");
        }
    });

};

Connector.prototype.doAction = function(action, params) {
    var actions    = action.split(".");
    var wrapper    = actions.reduce(function(result,name,index){
        
        if (result.po[name]) {
            if (util.types.isProxy(result.po[name]) && index < actions.length - 1){
                result.po      = result.po[name];
            } else if (@.is(result.po[name],Function)){
                result.functor = result.po[name];
            } else {
                result.po      = result.po[name];
            }
        }

        return result;
    },{
        po      : this,
        functor : null
    });

    if (wrapper.functor){
        return @.async.ensure(wrapper.functor.apply(wrapper.po,params));
    } else {
        return @.async.reject("Action not found : " + action);
    }
}

Connector.prototype.query = function(models, form) {

    if (!models) {
        throw new Error("models not found");
    }

    var lab = this;

    if (@.is(models, String)) {
        models = [models];
    } else if (!Array.isArray(models)) {
        throw new Error("models can be string or array");
    }

    form = @.merge.advanced({
        "!defaultValue": {},
        "fields": {
            "!valueType": "array",
            "!operation": "overwrite",
            "!defaultValue": []
        },
        "start": {
            "!valueType": "number",
            "!defaultValue": 0
        },
        "limit": {
            "!valueType": "number",
            "!defaultValue": -1
        },
        "conditions": {
            "!valueType": "array",
            "!operation": "overwrite",
            "!stringDelimiter": [",", " "],
            "!ignoreEmptyElement": true,
            "!autotrimString": true,
            "!arrayElement": {
                "!valueType": "string"
            },
            "!defaultValue": []
        },
        "params": {
            "!valueType": "array",
            "!operation": "overwrite",
            "!defaultValue": []
        },
        "sort": {
            "!valueType": "string",
            "!defaultValue": ""
        },
        "mapper": {
            "!valueType": "function"
        },
        "group" : {
            "!valueType": "string",
            "!defaultValue": ""
        }
    }, form);

    var querySQL = "select " + (form.fields.length ? form.fields.join(",") : "*") + " from " + models.join(" ") + (form.conditions.length ? (" where (" + form.conditions.join(") and (") + ")"  ) : "");

    if (form.group) {
        querySQL += " group by " + form.group;
    }

    if (form.limit < 0) {
        if (form.sort) {
            querySQL += " order by " + form.sort;
        }
    } else {
        if (form.sort) {
            querySQL += " order by " + form.sort + " limit ?,?";
        } else {
            querySQL += " limit ?,?";
        }
    }

    var countSQL = "select count(*) as count from " + models.join(" ") + (form.conditions.length ? (" where (" + form.conditions.join(") and (") + ")"  ) : "");

    return @.async(function() {
        @debug(querySQL);
        lab.sqlite.query.apply(lab.sqlite, [querySQL].concat(form.params.filter(function(param) {
            return param != undefined;
        })).concat(form.limit < 0 ? [] : [form.start, form.limit])).pipe(this);
    }).then(function(result) {

        if (form.mapper) {
            this.pool.list = result.records.map(form.mapper);
        } else {
            this.pool.list = result.records;
        }

        if (form.limit < 0) {
            this.next(result.records.length);
        } else {
            @debug(countSQL);
            lab.sqlite.query.apply(lab.sqlite, [countSQL].concat(form.params.filter(function(param) {
                return param != undefined;
            }))).then(function(result) {
                this.next(result.records[0].count);
            }).pipe(this);
        }

    }).then(function(total) {
        this.next({
            list: this.pool.list,
            total: total
        })
    })

};

Connector.prototype.save = function(model, object) {
    var lab = this;

    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model not found");
    }

    if (!(object && @.is(object, Object))) {
        throw new Error("object not valid");
    }

    return @.async(function() {

        if (object.id) {

            lab.find(model, object.id).then(function(existing_object) {

                var payload;

                if (existing_object) {
                    payload = @.merge.advanced(lab.models[model], existing_object, object);
                } else {
                    payload = @.merge.advanced(lab.models[model], object);
                }

                lab.sqlite.save(model, payload).then(function() {
                    this.next(lab.cacheObject(model, payload));
                }).pipe(this);

            }).pipe(this);

        } else {
            lab.create(model, object).pipe(this);
        }
    })
};

/**
 * 为什么这里要用advanced merge, 原因是当一个字段被设了默认值后， 
 * 通过advanced merge处理过后的对象，才是完备的，可查询的，所以需要把所有可能的对象形态，以advanced merge去处理。
 */

Connector.prototype.update = function(model, object, ...merged) {
    var lab = this;

    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model not found");
    }

    var objectID = findObjectID(object);

    return lab.find(model, objectID).then(function(cached) {
        
        if (cached) {

            var payload;

            if (@.is(object, Object)) {
                payload = @.merge.advanced.apply(null,[lab.models[model], cached, object].concat(merged));
            } else if (@.is(object, String)) {
                payload = @.merge.advanced.apply(null,[lab.models[model], cached].concat(merged));
            }

            lab.sqlite.save(model, payload).then(function() {
                this.next(lab.cacheObject(model, payload));
            }).pipe(this);

        } else {
            @dump(objectID)
            this.reject("object not found");
        }

    })
};

Connector.prototype.updateWithoutCache = function(model, object) {
    var lab = this;

    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model not found");
    }

    var objectID = findObjectID(object);

    return lab.find(model, objectID).then(function(cached) {

        if (cached) {
            lab.sqlite.save(model, object).then(function() {
                this.next(lab.cacheObject(model, object));
            }).pipe(this);
        } else {
            lab.create(model, object).pipe(this)
        }

    })
};

Connector.prototype.createWithoutCache = function(model, object) {
    var lab = this;


    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model not found");
    }

    return @.async(function() {
        var payload = @.merge.advanced(lab.models[model], object);
        if (!payload.id) {
            this.reject("create without cache must be id listed");
        } else {
            lab.sqlite.save(model, payload).resolve(payload).pipe(this);
        }
    });
}

Connector.prototype.create = function(model, object) {

    var lab = this;


    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model not found");
    }

    return @.async(function() {
        var payload = @.merge.advanced(lab.models[model], object);

        if (payload.id) {

            lab.find(model, payload.id).then(function(cached) {

                if (cached) {
                    this.reject(model + " object already exists : " + cached.id);
                } else {
                    this.next(payload);
                }

            }).pipe(this);

        } else {
            if (lab.models[model].id["!valueType"] != "number") {
                payload.id = @.uuid();
            }
            this.next(payload);
        }

    }).then(function(payload) {

        lab.sqlite.save(model, payload).then(function() {
            this.next(lab.cacheObject(model, payload));
        }).pipe(this);
    })

}

Connector.prototype.find = function(model, id) {
    var lab = this;

    if (!id) {
        return @.async.reject("id not found");
    }


    if (!model || !@.is(model, String) || !lab.models[model]) {
        throw new Error("model " + model +  " not found");
    }

    var condition = {};

    if (@.is(id, String)) {
        condition.id = id;
    } else if (@.is(id, Object)) {
        condition = id;
    } else {
        throw new Error("id format error");
    }

    return @.async(function() {

        if (lab.id && lab.objects && lab.objects[model] && lab.objects[model][id] && @.is(id, String)) {
            this.next(lab.objects[model][id]);
        } else {

            lab.sqlite.find(model, condition).then(function(result) {
                if (result.records.length) {
                    this.next(lab.cacheObject(model, result.records[result.records.length - 1]));
                } else {
                    this.next();
                }

            }).pipe(this);

        }

    })

}

Connector.prototype.readonly = function(predefined, callback) {
    
};

Connector.prototype.snapshot = function(predefined, callback) {

    var connection = this;

    if (arguments.length == 1 && @.is(predefined, Function)) {
        callback = predefined;
    } else if (arguments.length == 2 && @.is(callback, Function)) {
        predefined = predefined || {};
    } else {
        return @.async.reject("callback is not function");
    }

    if (connection.id) {

        return @.async(function() {

            var callbackResult = callback(connection);

            if (callbackResult && @.async.isAsync(callbackResult)) {
                callbackResult.pipe(this);
            } else {
                this.next();
            }
        });

    } else {

        return connection.sqlite.transaction(function(sqlite) {

            return @.async(function() {
                var superAsync = this;

                var snapshot = {
                    "id": connection.nextID++,
                    "sqlite": sqlite,
                    "objects": {},
                    "instances": {},
                    "services": connection.services
                };

                var proxy_getter = function(target, name) {

                    if (target[name]) {
                        return target[name];
                    } else if (target.services[name]) {

                        if (@.is(target.services[name], String)) {

                            if (!target.instances[target.services[name]]) {
                                target.instances[target.services[name]] = connection.bindings[target.services[name]](proxy);
                                Object.assign(target, target.instances[target.services[name]]);
                            }

                        } else {

                            target[name] = new Proxy({
                                id: target.id,
                                sqlite: target.sqlite,
                                objects: target.objects,
                                services: target.services[name],
                                instances: target.instances
                            }, {
                                get: proxy_getter
                            });

                        }

                        return target[name];

                    } else {
                        return connection[name];

                    }
                }

                var proxy = new Proxy(snapshot, {
                    "get": proxy_getter
                });

                var callbackResult = callback(proxy);

                if (callbackResult && @.async.isAsync(callbackResult)) {
                    callbackResult.pipe(this);
                } else {
                    this.next();
                }
            })
        });
    }
}

module.exports = Connector;
